require 'iconv'
require_relative '../smpp_client_error'

module SmppClient

  module Util

    # Converts Ruby types forth and back to protocol types and returns them as hexadecimal byte array.
    # Before all read_* and remove_* operations check buffer length enough for that operation. All append_*
    # methods return ByteBuffer object itself. So it's possible create chain of appenders:
    #  ByteBuffer.new.append_byte(1).append_short(2).append_int(3).append_bytes([4, 5, 6])

    class ByteBuffer

      # C-Octet String encoding and default Octet String encoding.
      ASCII = 'US-ASCII'

      # 1-Octet value, called Byte for short. 1 byte length.
      BYTE_SZ = 1
      # 2-Octet value, called Short for short. 2 bytes length.
      SHORT_SZ = 2
      # 4-Octet value, called Int for short. 4 bytes length.
      INT_SZ = 4

      # Protocol Byte max value.
      BYTE_MAX_VAL = 255
      # Protocol Short max value.
      SHORT_MAX_VAL = 65535
      # Protocol Int max value.
      INT_MAX_VAL = 4294967295

      # Protocol Octet mask.
      OCTET_MASK = 0xff
      # Protocol half-Octet mask. Used for buffer textual representation.
      HALF_OCTET_MASK = 0x0f

      # Octet byte array. Each value is Fixnum and not more than 255.
      attr_reader :bytes

      # Octet byte array may be supplied.
      def initialize(buffer = [])
        @bytes = buffer
      end

      # Buffer size.
      def size
        @bytes.size
      end

      # Append arbitrary byte array without any checks. Octet byte array expected.
      def append_bytes(bytes)
        if bytes.size > 0
          @bytes += bytes
        end
        return self
      end

      # Append Byte value with BYTE_SZ size and BYTE_MAX_VAL max value.
      def append_byte(byte_value)
        if byte_value >= 0 && byte_value <= BYTE_MAX_VAL
          @bytes << byte_value
        end
        return self
      end

      # Append Short value with SHORT_SZ size and SHORT_MAX_VAL max value.
      def append_short(short_value)
        if short_value >= 0 && short_value <= SHORT_MAX_VAL
          @bytes << (short_value >> 8 & OCTET_MASK)
          @bytes << (short_value & OCTET_MASK)
        end
        return self
      end

      # Append Byte value with INT_SZ size and INT_MAX_VAL max value.
      def append_int(int_value)
        if int_value >= 0 && int_value <= INT_MAX_VAL
          @bytes << (int_value >> 24 & OCTET_MASK)
          @bytes << (int_value >> 16 & OCTET_MASK)
          @bytes << (int_value >> 8 & OCTET_MASK)
          @bytes << (int_value & OCTET_MASK)
        end
        return self
      end

      # Append C-Octet string.String should contain only US-ASCII encoding characters.
      def append_cstring(string)
        if string.ascii_only?
          @bytes += string.bytes.to_a
          @bytes << 0
        end
        return self
      end

      # Append Octet string with any system supported encoding. ASCII by default.
      def append_string(string, encoding = ASCII)
        @bytes += string.encode(encoding).bytes.to_a
        return self
      end

      # Read Byte value from buffer.
      def read_byte
        @bytes[0]
      end

      # Read Short value from buffer.
      def read_short
        result = 0
        result |= @bytes[0] & OCTET_MASK
        result <<= 8
        result |= @bytes[1] & OCTET_MASK
      end

      # Read Int value from buffer.
      def read_int
        result = 0
        result |= @bytes[0] & OCTET_MASK
        result <<= 8
        result |= @bytes[1] & OCTET_MASK
        result <<= 8
        result |= @bytes[2] & OCTET_MASK
        result <<= 8
        result |= @bytes[3] & OCTET_MASK
      end

      # Remove Byte value from buffer.
      def remove_byte
        result = read_byte
        @bytes = @bytes.drop(BYTE_SZ)
        return result
      end

      # Remove Short value from buffer.
      def remove_short
        result = read_short
        @bytes = @bytes.drop(SHORT_SZ)
        return result
      end

      # Remove Int value from buffer.
      def remove_int
        result = read_int
        @bytes = @bytes.drop(INT_SZ)
        return result
      end

      # Remove C-Octet string value from buffer.
      def remove_cstring
        zero_pos = -1
        @bytes.each.with_index do |value, index|
          if value == 0
            zero_pos = index
            break
          end
        end
        if zero_pos >= 0
          result = @bytes[0, zero_pos].pack('C*')
          @bytes = @bytes.drop(zero_pos + 1)
          return result
        else
          raise SmppClient::SmppClientError, 'CString end not found.'
        end
      end

      # Remove Octet string. Length parameter should be in Octets.
      def remove_string(length, encoding = ASCII)
        result = @bytes[0, length].pack('C*')
        result = Iconv.iconv('UTF-8', encoding, result).join
        @bytes = @bytes.drop(length)
        return result
      end

      # Remove arbitrary count of bytes and return removed bytes as ByteBuffer.
      def remove_bytes(count)
        result = ByteBuffer.new(@bytes[0...count])
        @bytes = @bytes.drop(count)
        return result
      end

      # Convert buffer to hex string.
      def to_s
        result = ""
        @bytes.each do |value|
          result += Integer(value >> 4 & HALF_OCTET_MASK).to_s(16)
          result += Integer(value & HALF_OCTET_MASK).to_s(16)
        end
        return result
      end

    end

  end
  
end